﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Algorithms.Common;
using Dqaqs.Common;
using Dqaqs.Data;
using Dqaqs.Data.Extensions;
using Dqaqs.ProfileTableAlgorithm.Helpers;

namespace Dqaqs.ProfileTableAlgorithm
{
	public class ProfileTableIml : IDataAlgorithmImplementer
	{
		#region Fields

		private long _runningTime;
		private readonly IEpsilonReducerService _epsilonReducer;
		readonly Regex _headerItem = new Regex(@".*\(\s*(\w+)\).*", RegexOptions.Compiled);
		private DataTable _result;
		private IEnumerable<Tuple<string, string>> _headerTpl;
		private const string GroupingIdCol = "grp_id";

		#endregion

		#region Constructors

		public ProfileTableIml()
		{
			_epsilonReducer = new EpsilonReducerService(GroupingIdCol);
		}

		#endregion

		#region API

	    string[][,] IDataAlgorithmImplementer.Main(params string[] args)
		{
            //http://localhost:5512/ProfileTable/Create/ProfileTableAlgorithm/1,0.2,C:/Tmp/Out.csv,C:/Tmp/Data.csv
			var tpl = Arguments.ParseIdss(args, "tau, epsilon, InputFilepath, OutputFilePath");
			var filePath = tpl.Item4;
			var outPath = tpl.Item3;
			var epsilon = tpl.Item2;
			var tau = tpl.Item1;

			ExecuteAlgorithm(filePath, tau, epsilon);
			var rv = GetReturnArray();
			WriteOutput(outPath, rv);
			return new[]
			       	{
			       		new[,]
			       			{
			       				{"Running time", _runningTime.ToString(CultureInfo.InvariantCulture)},
			       				{"Data Size", _result.Rows.Count.ToString(CultureInfo.InvariantCulture)}
			       			},
			       		rv
			       	};
		}

		#endregion

		#region Private

		private static void WriteOutput(string outPath, string[,] rv)
		{
			var sb = new StringBuilder();
			for (int i = 0; i <= rv.GetUpperBound(0); i++)
			{
				for (int j=0; j <= rv.GetUpperBound(1); j++)
				{
					if (j!=0)
						sb.Append(",");
					sb.Append(rv[i, j]);
				}
				sb.AppendLine();
			}
			File.WriteAllText(outPath, sb.ToString());
		}

		private string[,] GetReturnArray()
		{
			var headers = new[] {"GroupingId (integer)"}.Concat(
				_headerTpl.Select(h => string.Format("{0} ( {1} )", h.Item1, h.Item2))
				)
				.Concat(new[] {"Count (integer)"})
				.ToArray();
			return Arguments.ReturnValue(_result, headers);
		}

		private void ExecuteAlgorithm(string file, int t, double e)
		{
			_runningTime = 0;
			var lines = File.ReadAllLines(file);
			var header = lines[0].Split(",".ToCharArray());
			_headerTpl = ValidateHeader(header).ToList();
			if (_headerTpl.Any(h => h.Item1.Contains(".") && !h.Item2.Equals("integer", StringComparison.InvariantCultureIgnoreCase)))
				throw new InvalidOperationException("Any column name with a '.' is a metric definition and must be of type 'integer'.");
			var schema = GetSchemaFromHeader(_headerTpl, Path.GetFileName(file));
			var metrics = schema.GetMetrics(schema.Tables[0]);
			if (!metrics.Any())
				throw new InvalidOperationException("Uploaded data should contian at least one column of type integer: Column.Metric");
			if (!metrics.All(m => schema.Tables[0].Columns.Any(c => c.Name.Equals(m.Key))))
				throw new InvalidOperationException("Any column name with a '.' is a metric definition. One or more metric definition refer to columns that are not already defined.");

			var guid = new Guid("97DA6F25-CB48-4C05-8340-2BAB94995261");
			Crud.CreateTable(guid, 1, schema.Tables[0].Name, schema.ToString());
			var tbl = Crud.UploadData(header, schema.Tables[0], lines, guid, 1, schema.Tables[0].Name, schema);
			var columns = _headerTpl.Where(tpl => !tpl.Item1.Contains(".")).Select(tpl => string.Format("[{0}]", tpl.Item1)).ToList();
			var orderedCols = GetColsOrderByDomainSizes(columns, tbl).ToList();
			var selectCols = string.Join(", ", orderedCols);
		    var metricCols = _headerTpl.Where(tpl => tpl.Item1.Contains("."))
		        .Select(
		            tpl => Tuple.Create(tpl.Item1.Split(".".ToCharArray())[0], tpl.Item1.Split(".".ToCharArray())[1])).ToList();
			var metricFields = string.Join(", ", metricCols.Select(tp => string.Format(" cast(sum([{0}.{1}]) as float)/count(*) as [Q({0}.{1})]",tp.Item1, tp.Item2)));
			var query = string.Format(" select GROUPING_ID({0}) AS grp_id, {0}, {1}, count (*) AS cnt from [{2}] group by ROLLUP ({0}) having count(*) > {3} order by {0}",
			                          selectCols, metricFields, tbl, t);
			var data = Crud.RunQuery(query);
			data = _epsilonReducer.ReduceData(data, e);
		    _result = TableHelper.ReordrColumns(data,
		                                        new[] {"grp_id"}.Concat(
		                                            orderedCols
		                                                .Concat(
		                                                    metricCols.Select(
		                                                        tp => string.Format("Q({0}.{1})", tp.Item1, tp.Item2)))
		                                                    .Concat(new[] {"cnt"})));
		}

		/// <summary>
		/// Returns the columns in the ascending order of domain size
		/// </summary>
		private static IEnumerable<string> GetColsOrderByDomainSizes(IEnumerable<string> columns, string tbl)
		{
			var distinctClauses = columns.Select(c => string.Format("count(distinct {0}) AS {0}", c));
			var sql = string.Format(@" select {0} from [{1}]", String.Join(", ", distinctClauses), tbl);
			var rv = Crud.RunQuery(sql);
			var cols = rv.Columns.Cast<DataColumn>().Select(c => new {Name = c.ColumnName, Val = rv.Rows[0][c.ColumnName]})
				.OrderBy(cv => cv.Val)
				.Select(cv => cv.Name)
				.ToList();
			return cols;
		}

		private static Schema GetSchemaFromHeader(IEnumerable<Tuple<string, string>> header, string tableName)
		{
			var rv = new Schema();
			var tbl = new SchemaTable
			          	{
			          		Name = tableName,
			          		Columns =
			          			header.Select(h => new SchemaColumn { Name = h.Item1, Type = h.Item2.ToLowerInvariant() }).ToArray()
			          	};
			rv.Tables = new[] { tbl };
			return rv;
		}

		private IEnumerable<Tuple<string, string>> ValidateHeader(IEnumerable<string> header)
		{
			var rv = new List<Tuple<string, string>>();
			foreach (var h in header)
			{
				var match = _headerItem.Match(h);
				if (!match.Success || !2.Equals(match.Groups.Count))
					throw new InvalidOperationException(
						string.Format("Header item {0} has wrong format. Accepted format is: FieldName(FieldType).", h));
				var type = match.Groups[1].Value;
				var supportedTypes = new[] { "integer", "datetime", "string" };
				if (!supportedTypes.Any(i => type.Equals(i, StringComparison.InvariantCultureIgnoreCase)))
					throw new InvalidCastException(string.Format("Type presented in header {0} is not supported. Supported Types are {1}"
					                                             , h
					                                             , supportedTypes.Aggregate(
					                                             	new StringBuilder()
					                                             	, (acc, item) => acc.Append(item + ","))));
				var hName = h.Split("(".ToCharArray(), 2)[0].Trim().ToLower();
				rv.Add(Tuple.Create(hName, type));
			}
			return rv;
		}

		#endregion
	}
}
